newGRAPH - Plug-in Developer Guide

Home Download & Installation Users Guide Plug-in Developer Guide newGRAPH forum



1 - Introduction

New functionalities for the newGRAPH could be easily programmed and plugged in the program. Additional invariants, graph generators, graph operations and the support for new graph file formats are all the areas which could be easily extended. The only prerequisite that the user needs is some basic knowledge of Java, and everything else could be learned in about an hour, the time needed to go through this tutorial.

Note: This tutorial requires Java SDK 1.5 or higher to be installed. It can be downloaded from http://java.sun.com. It also assumes that a copy of newGRAPH is installed.

2 - Invariants

This section explains how to create new invariants and plug them into the newGRAPH. It also explains how to plug in custom created parts of the newGRAPH.

2 - 1 - Hello, World!

We'll start by creating an invariant which simply displays the text "Hello, World!". A new file named HelloWorldInvariant.java should be created with any text editor. The following Java source code should be copy/pasted into the editor, and the file should be saved.

import scg.ac.ni.pmf.newgraph.graphdata.GraphData; import scg.ac.ni.pmf.newgraph.invariants.InvariantString; public class HelloWorldInvariant implements InvariantString { public String getName() { return "Hello World"; } public String getDescription() { return "The invariant displays some text."; } public String calculate(GraphData data) { return "Hello, World!"; } }

The java file is compiled by calling the javac compiler. Additionally, a path to the newgraph-api.jar archive must be specified as a classpath. In the case the newGRAPH is installed in the C:\Program Files directory, the command line will look like:

javac -classpath "c:\Program Files\newGRAPH\lib\newgraph-api.jar HelloWorldInvariant.java

It will create a file HelloWorldInvariant.class. This file should be copied to the directory newGRAPH/plugins/classes. After this, newGRAPH should be started, or restarted, if it is already running, and the invariant will appear in the list of invariants.


Figure 2.1: The invariant "Hello World" appears in the list of invariants

Let's inspect the code of the invariant. The name of the invariant is specified in the method getName. The name is displayed in the left column of the invariants table.

public String getName() { return "Hello World"; }

The description of the invariant comes next. It is retrieved through the method getDescription:

public String getDescription() { return "The invariant displays some text."; }

Finally, the method calculate calculates the value of the invariant. In our case, it simply retrieves the "Hello World" message:

public String calculate(GraphData data) { return "Hello, World!"; }


2 - 2 - Inspecting graph

To calculate an invariant, one has to get a full control of how the graph looks like. The class GraphData, which is supplied as a parameter to the method calculate does just that - reports the graph structure. It has a large number of helper methods, which are listed and described in the API, but right now we will concentrate on just a few. For instance, the method int getOrder() returns the number of vertices in the graph. We will create an invariant which retrieves just that, the invariant "Order".

But first, let's come back to the invariant "Hello World". It is of the type InvariantString, which means that it retrieves a string as its result.

public class HelloWorldInvariant implements InvariantString {

This would also fit the invariant Order, because the number of vertices is a string after all, but more suitable invariant type will be InvariantInt, which is the type of invariant which retrieves an integer object. Differenting types of invariants is important when it comes to some advanced functions of the system, like visuelization and invariant chaining. The list of the invariant types, and the types of results they produce, is given in the table 2.2.

Type Returns
InvariantBoolean boolean
InvariantDouble double
InvariantDoubleArray double []
InvariantDoubleEdge assigns double values to edges through EdgeDoubleValues
InvariantDoubleMatrix double [][]
InvariantDoubleVertex assigns double values to vertices through VertexDoubleValues
InvariantEdge EdgeData
InvariantEdgeArray EdgeData []
InvariantInt int
InvariantIntArray int []
InvariantIntEdge assigns int values to edges through EdgeIntValues
InvariantIntMatrix int [][]
InvariantIntVertex assigns int values to vertices through VertexIntValues
InvariantString String
InvariantVertex VertexData
InvariantVertexArray VertexData []

Table 2.2: Types of invariants

We should now create a new file named OrderInvariant.java in the text editor. The starting structure of the file will be similar to the invariant "Hello World":

import scg.ac.ni.pmf.newgraph.graphdata.GraphData; import scg.ac.ni.pmf.newgraph.invariants.InvariantInt; public class OrderInvariant implements InvariantInt { public String getName() { return "Order"; } public String getDescription() { return "The number of vertices in the graph."; } public int calculate(GraphData data) { return 0; } }

Compile and save the invariant class in the plugins directory. After the newGRAPH is restarted, it will display 0 as the number of vertices, which is not actually what we would like to have. Using the method int getOrder():

public int calculate(GraphData data) { return data.getOrder(); }

, the correct value is produced.

In the next example, we would like to know the number of vertices of the degree 1. The best way to program this would be to go through all the vertices and inspect their degree. The method Enumeration vertices() returns an enumeration on all the vertices in the graph. Each element of the enumeration is of the type VertexData. This class is to the vertex as the GraphData is to the graph, it reports data about the vertex. It is better described in the plugin API, now we will only need the method int degree(), which retrieves the degree of the vertex. For this occasion, we can make another invariant, named "Order One":

import java.util.Enumeration; import scg.ac.ni.pmf.newgraph.graphdata.GraphData; import scg.ac.ni.pmf.newgraph.graphdata.VertexData; import scg.ac.ni.pmf.newgraph.invariants.InvariantInt; public class OrderOneInvariant implements InvariantInt { public String getName() { return "Order one"; } public String getDescription() { return "The number of vertices of the degree one."; } public int calculate(GraphData data) { int n = 0; for(Enumeration i = data.vertices(); i.hasMoreElements(); ) { VertexData vertex = (VertexData) i.nextElement(); if(vertex.degree() == 1) n++; } return n; } }


2 - 3 - Parametrization

What if we would like to get the number of vertices of the degree that the user enters? In the following example, we will create the invariant which gives the number of vertices of the degree K, where K is the parameter entered by the user. For start, we will create the invariant "Order K", looking almost exactly the same as the previous example:

import java.util.Enumeration; import scg.ac.ni.pmf.newgraph.graphdata.GraphData; import scg.ac.ni.pmf.newgraph.graphdata.VertexData; import scg.ac.ni.pmf.newgraph.invariants.InvariantInt; public class OrderKInvariant implements InvariantInt { public String getName() { return "Order K"; } public String getDescription() { return "The number of vertices of the degree K."; } public int calculate(GraphData data) { int n = 0; for(Enumeration i = data.vertices(); i.hasMoreElements(); ) { VertexData vertex = (VertexData) i.nextElement(); if(vertex.degree() == 1) n++; } return n; } }

Having a parametrization requires the invariant to fullfill a few requirements. First, each parameter must have the standard JavaBean methods for getting and setting the parameter value, <type> get<param_name>() and void set<param_name>(<type>). Such methods for the integer parameter named "K" will look like:

private int k; public int getK() { return k; } public void setK(int k) { this.k = k; }

Additionally, the whole class must implement the interface Parametrizable and the method String checkParameters(). This method should checks the parameters and retrieve the error string if the parameter values are invalid. If the values are valid, the method should return null. In this example, we will require that the parameter K has positive value:

public class OrderKInvariant implements InvariantInt, Parametrizable { ... public String checkParameters() { if(k <= 0) return "K must be positive"; else return null; }

We should change the method calculate to use the variable K for the checked number of vertices. The complete source code of the invariant is:

import java.util.Enumeration; import scg.ac.ni.pmf.newgraph.graphdata.GraphData; import scg.ac.ni.pmf.newgraph.graphdata.VertexData; import scg.ac.ni.pmf.newgraph.invariants.InvariantInt; import scg.ac.ni.pmf.newgraph.param.Parametrizable; public class OrderKInvariant implements InvariantInt, Parametrizable { public String getName() { return "Order K"; } public String getDescription() { return "The number of vertices of the degree K."; } public int calculate(GraphData data) { int n = 0; for(Enumeration i = data.vertices(); i.hasMoreElements(); ) { VertexData vertex = (VertexData) i.nextElement(); if(vertex.degree() == k) n++; } return n; } private int k; public int getK() { return k; } public void setK(int k) { this.k = k; } public String checkParameters() { if(k <= 0) return "K must be positive"; else return null; } }

Upon installing the invariant "Order K" and restarting newGRAPH, by double clicking the invariant, we are able to set the parameter K:


Figure 2.3: Entering parameters for the invariant "Order K"

The table 2.4 gives an overview of the classes that could be used for parameters, and how would they appear in the dialog for parametrization.

Type Appearance in the parametrization dialog
boolean Checkbox
byte Select box
double Text input
GraphData Select box on the open graphs
int Text input

Table 2.4: Types of parameters

The only parameter that needs a special attention is the parameter byte. It is used to let the user select one of the given choices in the select box. For instance, we would like to modify the invariant "Order K" so that it can retrieves the number of vertices of the order <= K, = K or >= K. Therefore, we introduce another parameter, named "Compare", which indicates which comparison takes place. The parameter must be of the type byte:

private byte compare = 1; public byte getCompare() { return compare; } public void setCompare(byte compare) { this.compare = compare; }

Additionally, we must provide the set of choices. We do it by specifying a set of public static final byte variables, whose name begins with the name of the invariant and underscore. For example, our choices will be:

public static final byte COMPARE_Less_Or_Equals = 1; public static final byte COMPARE_Equals = 2; public static final byte COMPARE_Greater_Or_Equals = 3;

Finally, we change the method calculate, to include the functionality of the new parameter:

public int calculate(GraphData data) { int n = 0; for(Enumeration i = data.vertices(); i.hasMoreElements(); ) { VertexData vertex = (VertexData) i.nextElement(); if(compare == COMPARE_Less_Or_Equals) { if(vertex.degree() <= k) n++; } else if(compare == COMPARE_Equals) { if(vertex.degree() == k) n++; } else if(compare == COMPARE_Greater_Or_Equals) { if(vertex.degree() >= k) n++; } } return n; }

We will end with specifying the parameter description. Let's say that we don't like the text "Compare", that appears in the parametrization dialog. To give a better explanation of a parameter, a method String get<param>Name() should be implemented. In this example, we will replace it with the text "Vertex degree compared to K":

public String getCompareDesc() { return "Vertex degree compared to K"; }

The new text appears in the parametrization dialog:


Figure 2.5: Parameter descriptions.



2 - 4 - Invariant chaining

A result of an invariant could be simply used for calculating some other invariant. It is just important that both invariants are installed at the same time. In the following example, we are using the result of the invariant Spectrum, which returns the spectrum of the graph, to calculate the energy of the graph:

private Spectrum spectrum; public double calculate(GraphData data) { double[] eig = spectrum.calculate(data); double energy = 0; for(int i = 0; i < eig.length; i++) energy += Math.abs(eig[i]); return energy; }


2 - 5 - Weights

As an additional parameter, we may require the user to provide weights on vertices and edges. For instance, we may wish to make an invariant which calculates distances in a weighted graph. Weights are supplied as parameters of the type weight.

private WeightEdgeInt wd; public WeightEdgeInt getWD() { return wd; } public void setWD(WeightEdgeInt wd) { this.wd = wd; }

To get a value of the weight on an edge or vertex, one should use the method getWeight:

EdgeData edge; WeightEdgeInt wd; ... int value = data.getWeight(edge, wd);

There are four types of weights, depending on whether they weigh vertices or edges, and depending of whether the weight is an integer or a real number.

Class Weight type
WeightVertexInt Weighs vertices with integer numbers.
WeightVertexDouble Weighs vertices with real numbers.
WeightEdgeInt Weighs edges with integer numbers.
WeightEdgeDouble Weighs edges with real numbers.

Table 2.6: Types of weights



3 - Graph generators

Graph generators appear under the option New, as the last group of options in the submenu. They allow the user to generate graphs of a certain type. Graph generators follow the same principles as the graph invariants. They are installed the same way, and they support parametrization the same way. For the example, we will create the generator which produces binary trees of a certain depth.

We will create a file "BinaryTreeGen.java". The starting code for writing generators should look like:

import scg.ac.ni.pmf.newgraph.generator.GraphGenerator; import scg.ac.ni.pmf.newgraph.graphdata.Graph; public class BinaryTreeGen implements GraphGenerator { public String getName() { return "Binary Tree"; } public String getDescription() { return "Creates a binary tree of the desired depth."; } public void newGraph(Graph graph) { } }

After it is installed, it appears under the menu New:


Figure 3.1: Graph generator "Binary Tree" appears in the menu

Currently, it produces an empty graph. The method which creates the graph is void newGraph(Graph graph). In our example, this method will need parameter for the depth of the graph. Using the rules for parametrization from the previous section, we can add the new parameter.

import scg.ac.ni.pmf.newgraph.generator.GraphGenerator; import scg.ac.ni.pmf.newgraph.graphdata.Graph; import scg.ac.ni.pmf.newgraph.param.Parametrizable; public class BinaryTreeGen implements GraphGenerator, Parametrizable { public String getName() { return "Binary Tree"; } public String getDescription() { return "Creates a binary tree of the desired depth."; } public void newGraph(Graph graph) { } private int depth; public int getDepth() { return depth; } public void setDepth(int depth) { this.depth = depth; } public String checkParameters() { if(depth <= 0) return "Depth must be positive"; else return null; } }

Now we only have to create the method for generating the graph. The method newGraph is supplied with the instance of the class Graph. The class Graph has a number of methods for creating vertices, edges, deleting parts of the graph and handling many other graph properties, which are better described in the section API. Right now, the only two methods that we will need will be Vertex createVertex(double x, double y), which creates a vertex on the specified coordinates, and Edge createEdge(Vertex a, Vertex b), which connects the given vertices.

The tree will be created recursively. If the depth of the tree is 1, a single vertex will be created. If the depth is greater then 1, the root vertex will be created, and the method will be called for two subtrees for the depth - 1. The root will be connected with the roots of the two subtrees. Initially, the method will be called for the parameter depth.

public void newGraph(Graph graph) { generateTree(graph, depth, 0, 0); } private Vertex generateTree(Graph graph, int d, double x, double y) { Vertex v = graph.createVertex(x, y); if(d > 1) { double size = 10 * Math.pow(2, d - 1); Vertex a = generateTree(graph, d - 1, x - size, y + size); Vertex b = generateTree(graph, d - 1, x + size, y + size); graph.createEdge(v, a); graph.createEdge(v, b); } return v; }


4 - Graph actions

Graph actions change the graph during the graph editing. They are available from the menu Edit, or from the context menu shown by right clicking the graph edit area. They can change the graph, but they can also change the current selection, move vertices, and do all the transformations that the class Graph provides API.

All the rules for the invariants and generators apply for the graph actions. They could have additional parameters, and they are installed in the newGRAPH the same way. As an example graph action, we will create the action which duplicates every selected vertex.

We will create a file DoubleSelectedAction.java. The starting source code for the graph actions should look like this:

import scg.ac.ni.pmf.newgraph.action.GraphAction; import scg.ac.ni.pmf.newgraph.graphdata.Graph; import scg.ac.ni.pmf.newgraph.graphdata.GraphData; public class DoubleSelectedAction implements GraphAction { public String getName() { return "Double Selected Vertices"; } public String getDescription() { return "Doubles the selected vertices."; } public boolean isEnabled(GraphData graph) { return true; } public void action(Graph graph, boolean ctrl, boolean shift) { } }

Again, there are methods for specifying the name and the descriptions. The method void action(Graph graph, boolean ctrl, boolean shift) is the one responsible for what happens with the graph when the user clicks on the option. We will create a code which goes through all the selected vertices, creates a duplicate vertex and connects it to all the neighbors of the source vertex. We will use two new methods: Enumeration Graph.selectedVertices(), which returns the enumeration of all the selected vertices, and Enumeration Vertex.getNeighbors(), which returns enumeration on all the neighbors of the vertex. The resulting code will be:

public void action(Graph graph, boolean ctrl, boolean shift) { for(Enumeration i = graph.selectedVertices(); i.hasMoreElements(); ) { Vertex source = (Vertex) i.nextElement(); Vertex destination = graph.createVertex(source.getX() + 10, source.getY() + 10); for(Enumeration j = source.getNeighbors(); j.hasMoreElements(); ) { Vertex v = (Vertex) j.nextElement(); graph.createEdge(destination, v); } } }

The method action accepts two boolean parameters: ctrl and shift, which indicate were these buttons held when the action was triggered. The programmer can make the action have different effects accordingly.

If we take a look at the code, we will see that the action will do nothing if there are no selected vertices. Therefore, one might wish to turn the option off in such case. That's what the method isEnabled is for: it tells the system that the graph is in such state the the action can or can not be executed. We can add this check:

public boolean isEnabled(GraphData graph) { return graph.getSelectedVerticesNo() > 0; }

The actions from the "Edit" menu are divided into groups with the menu separators. The grouping is defined by the name of the action. If the name contains a dot, the part before the dot is considered the name of the group. For instance, the name of the action "Changers.Complement indicates that the group that the action belongs to is "Changers", while the title of the action is "Complement".


Figure 4.1: Graph actions menu

We will assign our newly created action the name "Changers.Double Selected Vertices", so that it appears with other actions whose primary functionalities are to change the structure of the graph:

public String getName() { return "Changers.Double Selected Vertices"; }

5 - Graph dependancies

A graph can be set to be a result of a transformation of some other graph. When the other graph changes, the dependant graph changes too. Thus, the user is able to track changes of on the resulting graph as he modifies the source graph. Graph dependancies are the plugins that establish such connections.

The structure of a graph dependancy is almost the same as the other plugins. They have to provide the name, description, and a method to create the dependant graph:

public interface DPD { public String getName(); public String getDescription(); public void create(Graph graph); }

To have a source graph, one should use the GraphData parameter, the usual way:

private GraphData source; private GraphData getSource() { return source; } public void setSource(GraphData source) { this.source = source; }

It is possible to use more graphs as parameters, as well as all the other parameters, such as constants and weights.

6 - File types

6 - 1 - Read and write

The user might need to open a graph from the file which is not readable by the newGRAPH, or he might wish to export the graph to a file of such type. The support for the new file types is another plug-and-play feature.

As an example, we will create a support for some fictional file type "GraphTxt". We'll start with a skeleton for the plugin, in a file named GraphTxtFileType.java:

import java.io.File; import scg.ac.ni.pmf.newgraph.graphdata.Graph; import scg.ac.ni.pmf.newgraph.graphdata.GraphData; import scg.ac.ni.pmf.newgraph.io.GraphFileType; import scg.ac.ni.pmf.newgraph.io.GraphRWException; public class GraphTxtFileType implements GraphFileType { public String getName() { return "Graph Txt"; } public String getExtension() { return "GraphTxt"; } public boolean accepts(File file) { return file.getName().endsWith(".GraphTxt"); } public void read(File file, Graph graph) throws GraphRWException { } public void write(File file, GraphData data) throws GraphRWException { } }

The method getName simply returns the name of the file type. The method getExtension returns the file extension that the IO plugin accepts. Similarily, the method boolean accepts(File) tells is the file accepted for reading. This method is important in the case there are more IO plugins which deal with the same file extension, to differentiate between the file types. Finally, the methods read and write handle the file input and output.

It is important to say that each vertex has four important properties: X, Y, ID and Label. The first two are the coordinates. The third one is an unique integer identificator, and the last one is the string label.

Our fictional file type has the following structure. The first line contains the number of vertices, say N. The next N lines contain information about the vertices, separated by comma: X, Y, ID and Label. The next line contains the number of edges, say M, and the next M lines contain the ID's of the vertices connected by the respected edge.

The method write gets two input parameters: File file, which should be written, and GraphData data, which provides information about the graph. The implementation of the method should be straightforward:

public void write(File file, GraphData data) throws GraphRWException { try { PrintWriter out = new PrintWriter(new FileWriter(file)); out.println(data.getOrder()); for(Enumeration i = data.vertices(); i.hasMoreElements(); ) { VertexData vertex = (VertexData) i.nextElement(); out.println(vertex.getX() + ", " + vertex.getY() + ", " + vertex.getID() + ", " + vertex.getLabel()); } out.println(data.getNumberOfEdges()); for(Enumeration i = data.edges(); i.hasMoreElements(); ) { EdgeData edge = (EdgeData) i.nextElement(); out.println(edge.getIDA() + ", " + edge.getIDB()); } out.close(); } catch(IOException e) { throw new GraphRWException(e.getMessage()); } }

The method read gets the file which should be read as the first parameter, whereas the second parameter is the graph that should be created. The implementation in our example will be:

public void read(File file, Graph graph) throws GraphRWException { try { BufferedReader in = new BufferedReader(new FileReader(file)); int n = Integer.parseInt(in.readLine()); for(int i = 0; i < n; i++) { StringTokenizer st = new StringTokenizer(in.readLine(), ", "); double x = Double.parseDouble(st.nextToken()); double y = Double.parseDouble(st.nextToken()); int id = Integer.parseInt(st.nextToken()); String label = st.nextToken(); try { graph.createVertex(id, x, y, label); } catch(DuplicateIDException e) { } catch(DuplicateLabelException e) { } } int m = Integer.parseInt(in.readLine()); for(int i = 0; i < m; i++) { StringTokenizer st = new StringTokenizer(in.readLine(), ", "); int idA = Integer.parseInt(st.nextToken()); int idB = Integer.parseInt(st.nextToken()); graph.createEdge(idA, idB); } } catch(IOException e) { throw new GraphRWException(e.getMessage()); } catch(NumberFormatException e) { throw new GraphRWException(e.getMessage()); } }

It's important to add that the only exception that should be thrown are GraphRWException. They should generally indicate that something is wrong with the file, and it can't be read. The message it gets as the parameter is the message that is displayed to the user in the alert box when he tries to open a faulty file. The best practice with other exceptions that may occur during the read is to catch them, and immediately propagate furtrer wrapped in a GraphRWException, the way it is shown:

} catch(IOException e) { throw new GraphRWException(e.getMessage()); } catch(NumberFormatException e) { throw new GraphRWException(e.getMessage()); }

6 - 2 - Exporters

Graph exporters are tools that provide one way exporting graphs to a certain file. The most common usage would be to provide exporting graphs in various graphics formats, such as BMP, GIF, EPS, PS, WMF and other formats. A plugin of this type should give the name, the extension that it works with, and the method for exporting graphs.

public interface GraphExporter { public void export(File file, GraphData data) throws GraphRWException, IOException; public String getName(); public String getExtension(); }

7 - Classes that represent graph

This section gives a brief overview of the classes that represent graph. To get the full reference, use the newGRAPH API.

There are two groups of classes that work with graphs: read-only classes and read-write classes. The read-only classes can only report the graph structure, whereas the read-write classes can change the graph structure.

Read-only Read-write
GraphData Graph
VertexData Vertex
EdgeData Edge

Table 7.1: Read-only and read-write classes

Each vertex in the graph has four main properties: X and Y coordinates, a unique integer ID, and a String Label. Vertices are created using the method Vertex Graph.createVertex(double x, double y), which takes the coordinates, and automatically assigns a new ID, and assigns a label that's equal to the ID. Another method, Vertex Graph.createVertex(int id, double x, double y, String label), creates a vertex with the desired ID and Label. In the case the specified ID already exists, this method throws a DuplicateIDException, because the ID number must be unique for each of the vertices. The same rule applies for the double labels, in which case a DuplicateLabelException is thrown.

To create an edge, the method createEdge(int idA, int idB) is used, which takes the IDs of the vertices which should be connected as parameters. There is another version of the method, createEdge(Vertex va, Vertex vb), which takes objects of the type Vertex. Likewise, there are many other methods which differ in the form of the parameter, but basically produce the same effect.

The set of selected vertices and edges is also managable through these classes. For instance, method boolean Vertex.isSelected() tell whether the vertex is selected, and the methods void Edge.select() and void Edge.deselect() select and deselect the edge. Since more vertices and edges could be selected at the same time, the first selected vertex and edge are distinctive. Some of the methods that access them are VertexData GraphData.getPrimarySelectedVertex() and EdgeData GraphData.getPrimarySelectedEdge().

Some of the methods address the file where the graph is stored. For exampe, String Graph.getAbsolutePath() returns the name of the file where the graph is stored, and boolean Graph.isDirty() tells was the graph changed after the last change.

8 - Final remarks

The complete API for the classes that are used for writing plugins for the newGRAPH is available here.

In the examples used in this manuals, the plugins were deployed as compliled java classes in the directory newGRAPH/plugins/classes. However, classes could be packaged into Jar or ZIP archives, and saved in the directory newGRAPH/plugins/lib. All libraries that some plugin or plugin libraries depend on must be also saved in the direcory newGRAPH/plugins/lib.